#!/usr/bin/env python
#
# sdr_script - User Interface for RX functions
# 
# User: bob
# Date: 02/12/14
# Copyright (C) 2013 by G3UKB Bob Cowdery
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#    
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#    
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#    
#  The author can be reached by email at:   
#     bob@bobcowdery.plus.com
#


# System imports
import os,sys
from PyQt4 import QtCore, QtGui
import pickle
import urllib
import traceback
import subprocess
from time import sleep
import logging
import copy

# Application imports
import common.instance_cache as instance_cache
from common.defs import *
from common import persist
from common import broker
from ui.receiver.rx_common import *
from ui.display import display
from ui.common import meter
from ui.common import vfo

"""
GUI class for a receiver
"""
class Receiver(QtGui.QMainWindow):
    
    def __init__(self, rx_id, ch_api, q):
        """
        Constructor
        
        Arguments:
            rx_id   -- the id of this receiver
            ch_api  -- the channel API object for this receiver
            q       -- Q to send messages to
            
        """
        
        super(Receiver, self).__init__()
        
        self.__q = q
        self.__rx_id = rx_id
        self.__ch_api = ch_api
        
        # Get the default or saved state 
        self.STATE_FILE = 'rx_%d.state' % (rx_id)
        self.__state = persist.getSavedState(self.STATE_FILE)
        if self.__state == None:
            # Apply defaults
            self.__state = DEFAULT_STATE
            persist.saveState(self.STATE_FILE, self.__state)
        self.__current_freq = self.__state[FREQ]
        self.__width = self.__state[WINDOW_WIDTH]
        self.__min_width = self.__state[WINDOW_MIN_WIDTH]
        self.__height = self.__state[WINDOW_HEIGHT]
        self.__displayWidth = self.__state[DISPLAY_WIDTH]
        self.__displayHeight = self.__state[DISPLAY_HEIGHT]
        self.__resize_to = (self.__width, self.__height)
        
        # Get the current settings
        self.__cfg = instance_cache.getInstance('Settings')
        self.__current_cfg = self.__cfg.copy()
    
        # Get our API instance
        self.__metis_api = instance_cache.getInstance('Metis_API')
        # and the controller instance
        self.__ctrl = instance_cache.getInstance('Controller')
                
        # Set the back colour
        palette = QtGui.QPalette()
        palette.setColor(QtGui.QPalette.Background,QtGui.QColor(43,63,68,255))
        self.setPalette(palette)
        
        # Create the display window
        self.__disp = None
        self.__disp = display.Panadapter(rx_id, self.__displayWidth, self.__displayHeight, self.__panFreq, self.__floatCallback, q)
        self.__disp.setBandwidth(float(self.__cfg[COMMON][IN_RATE])/1000000.0)
        
        # Initialise variables
        # Tells us when and how to switch antennas
        self.__antenna_plan = None
        self.__last_freq = None
        
        # Initialise the GUI
        self.initUI(rx_id)
        
        # Subscribe to events
        if rx_id == 0:
            broker.subscribe(self, TX_FREQ_EVNT)
            broker.subscribe(self, ANTENNA_EVNT)
        broker.subscribe(self, DISPLAY_EVNT)
            
        # Timer for background tasks
        self.__timer = QtCore.QTimer()
        self.__timer.setInterval(100)
        self.__timer.timeout.connect(self.on_timeout)
        self.__timer.start()
        
        # Set initial width in 200ms
        QtCore.QTimer.singleShot(200, self.__timeout)
    
    def create_display_unit(self, blk_sz):
        
        self.__blk_sz = blk_sz
        unit = self.__disp.create_display_unit(blk_sz, self.__cfg[RX][DISPLAYS][PAN_MODE], self.__cfg[RX][DISPLAYS][PAN_FRM_AV], self.__cfg[RX][DISPLAYS][WINDOW_TYPE], self.__cfg[RX][DISPLAYS][PAN_FFT_SZ])
        # Light the display window
        self.__disp.showDisplay()
        return unit
    
    def initUI(self, rx_id):
        
        try:
            self.setToolTip('SdrScript RX Interface [%d]' % (rx_id))
            QtGui.QToolTip.setFont(QtGui.QFont('SansSerif', 10))
            self.move(self.__state[WINDOW_X], self.__state[WINDOW_Y])
            self.setWindowTitle('SdrScript Receiver')
            # Turn off all title bar buttons
            self.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint)
            
            # Main panel and grid
            w = QtGui.QWidget()
            self.setCentralWidget(w)
            grid = QtGui.QGridLayout()
            w.setLayout(grid)
            
            # Top grid
            # Note add the grid directly as a layout not in a panel else space cannot be removed
            top_grid = QtGui.QGridLayout()
            grid.addLayout(top_grid, 0, 0)
            
            # Display the RX number
            rx_label = QtGui.QLabel('RX-%d' % (rx_id+1))
            rx_label.setMaximumHeight(15)
            top_grid.addWidget(rx_label, 0, 0)
            top_grid.setColumnStretch(0,0)
            rx_label.setStyleSheet("QLabel {color: orange;  font: bold 16px; border-style: none}")
            
            # Add meter control
            self.__meter = meter.Meter(w, CH_RX, M_RX_S_AV)
            top_grid.addWidget(self.__meter, 0, 1, -1, -1)
            top_grid.setRowMinimumHeight(0, 15)
            top_grid.setColumnStretch(1,1)
            
            # Add VFO control
            vfo_grid = QtGui.QGridLayout()
            grid.addLayout(vfo_grid, 1, 0)
            self.__vfo = vfo.Vfo(self.__ctrl, self.__metis_api, CH_RX, rx_id)
            self.__vfo.addVfo(self, vfo_grid, self.__current_freq)
            if self.__disp != None: self.__disp.setCenterFreq(self.__current_freq)
            
            # Add mode, filter, AGC and gain controls
            main_control_panel = QtGui.QWidget(w)
            grid.addWidget(main_control_panel, 2, 0, 1, 2)
            self.__addMainControls(main_control_panel)
            
            # Gain control
            gain_control_panel = QtGui.QWidget(w)
            grid.addWidget(gain_control_panel, 3, 0)
            self.__addGainControls(gain_control_panel)
            
            # Display
            grid.addWidget(self.__disp, 0, 4, 5, 1)
            grid.setColumnStretch(4,10)
            
            #self.addDockWidget(2, self.__disp)
            #self.setDockOptions(QtGui.QMainWindow.AnimatedDocks)
            
            w.setLayout(grid)
            
        except Exception as e:
            self.__q.put ((BOTH, logging.ERROR, 'py.rx_ui', 'Exception [%s][%s]' % (str(e), traceback.format_exc())))
    
    def closedown(self):
        """ Request to close """
        
        # Update window position
        position = self.pos()
        self.__state[WINDOW_X] = position.x()
        self.__state[WINDOW_Y] = position.y()
        self.__state[WINDOW_WIDTH] = self.width()
        self.__state[WINDOW_HEIGHT] = self.height()
        self.__state[DISPLAY_WIDTH] = self.__disp.width()
        self.__state[DISPLAY_HEIGHT] = self.__disp.height()
        # Frequency
        self.__state[FREQ] = self.__current_freq
        # Save to the state file
        persist.saveState(self.STATE_FILE, self.__state)
        # Stop the background timer
        self.__timer.stop()
        # Quit the windows
        self.__disp.closedown()
        self.close()
     
    def updateConfig(self):
        """ Update the backend """
        
        self.__metis_api.set_rx_freq(self.__rx_id, self.__current_freq)
        self.__ch_api.set_rx_mode(self.__state[MODE])
        self.__ch_api.set_agc_mode(self.__state[AGC])
        self.__ch_api.set_rx_gain(float(self.__state[GAIN])/100.0)
        if self.__rx_id == 0:
            # Global settings only attached to RX0
            self.__metis_api.set_alex_attn(self.__state[ATTN])
            self.__metis_api.set_preamp(self.__state[PREAMP_HF])
            self.__metis_api.set_alex_6m_preamp(self.__state[PREAMP_6M])
        
    def run(self):
        """ Show the window """
        
        self.show()
     
    # UI Actions ---------------------------------------------------------------
    def wheelEvent(self, event):
        """
        Mouse wheel event
        
        Arguments:
            event   --  event data
            
        """
        
        super(Receiver, self).wheelEvent(event)
        
        self.__current_freq = self.__vfo.wheelEvent(event.delta())
        if self.__disp != None:
            self.__disp.setCenterFreq(self.__current_freq)
               
    def on_mode(self, sel):
        """
        Mode change
        
        Arguments:
            event   --  event data
            
        """
     
        self.__state[MODE] = sel
        self.__ch_api.set_rx_mode(self.__state[MODE])
        self.__set_filter()
        
    def on_filter(self, sel):
        """
        Filter change
        
        Arguments:
            event   --  event data
            
        """
        
        self.__state[FILTER] = sel
        self.__set_filter()
        
    def on_agc(self, sel):
        """
        AGC change
        
        Arguments:
            sel   --  selected item
            
        """
        
        self.__state[AGC] = sel
        self.__ch_api.set_agc_mode(self.__state[AGC])
     
     
    def on_attn(self, sel):
        """
        Attenuator change
        
        Arguments:
            sel   --  selected item
            
        """
        
        self.__state[ATTN] = sel
        self.__metis_api.set_alex_attn(attn_lookup[sel][0])
    
    def on_preamp(self, sel):
        """
        HF preamp change
        
        Arguments:
            sel   --  selected item
            
        """
        
        self.__state[PREAMP_HF] = sel
        self.__metis_api.set_preamp(self.__state[PREAMP_HF])
    
    def on_preamp_6m(self, sel):
        """
        6M preamp change
        
        Arguments:
            sel   --  selected item
            
        """
        
        self.__state[PREAMP_6M] = sel
        self.__metis_api.set_alex_6m_preamp(self.__state[PREAMP_6M])
    
    def on_gain(self, value):
        """
        Gain change
        
        Arguments:
            event   --  event data
            
        """
        
        self.__state[GAIN] = value
        self.__ch_api.set_rx_gain(float(value)/100.0)
    
    def on_timeout(self):
        """
        Background processing
        
        Arguments:
            
        """
        
        db = self.__ch_api.get_rx_meter_data(M_RX_S_AV)
        if self.__meter != None: self.__meter.update_meter(db)
        
    # PRIVATE ==================================================================
    
    # Interactors --------------------------------------------------------------
    def __addMainControls(self, panel):
        """
        Add main controls:
            Mode
            Filter
            AGC
        
        Arguments:
            panel   -- bounding area for controls
            
        """    
        
        # Layout
        grid = QtGui.QGridLayout()
        grid.setColumnMinimumWidth(0, 80)
        grid.setColumnMinimumWidth(1, 80)
        grid.setColumnMinimumWidth(2, 80)
        grid.setColumnStretch(0,1)
        grid.setColumnStretch(1,1)
        grid.setColumnStretch(2,1)
        panel.setStyleSheet(".QWidget {border-style: solid; border-color: gray; border-width: 1px;}")
        
        # Populate
        # Mode
        mode_label = QtGui.QLabel('Mode')
        grid.addWidget(mode_label, 0, 0, QtCore.Qt.AlignCenter)
        mode_combo = QtGui.QComboBox()
        mode_list = []
        for mode in mode_lookup:
            mode_list.append(mode[1])
        mode_combo.addItems(mode_list)
        grid.addWidget(mode_combo, 1, 0)
        mode_combo.activated.connect(self.on_mode)
        # Set the current state
        mode_combo.setCurrentIndex(self.__state[MODE])
        
        # Filter
        filter_label = QtGui.QLabel('Filter')
        grid.addWidget(filter_label, 0, 1, QtCore.Qt.AlignCenter)
        filter_combo = QtGui.QComboBox()
        filter_list = []
        for filter in filter_lookup:
            filter_list.append(filter[3])
        filter_combo.addItems(filter_list)
        grid.addWidget(filter_combo, 1, 1)
        filter_combo.activated.connect(self.on_filter)
        # Set the current state
        filter_combo.setCurrentIndex(self.__state[FILTER])
        self.__ch_api.set_rx_filter_run(CH_BP_ON)
        self.__set_filter()
        
        # AGC
        agc_label = QtGui.QLabel('AGC')
        grid.addWidget(agc_label, 0, 2, QtCore.Qt.AlignCenter)
        agc_combo = QtGui.QComboBox()
        agc_list = []
        for agc in agc_lookup:
            agc_list.append(agc[1])
        agc_combo.addItems(agc_list)
        grid.addWidget(agc_combo, 1, 2)
        agc_combo.activated.connect(self.on_agc)
        # Set the current state
        agc_combo.setCurrentIndex(self.__state[AGC])

        panel.setLayout(grid)
        
    def __addGainControls(self, panel):
        """
        Add gain controls:
            Attenuator
            Preamps
            Volumn
        
        Arguments:
            panel   -- bounding area for controls
            
        """    
        
        # Layout
        grid = QtGui.QGridLayout()
        panel.setStyleSheet(".QWidget {border-style: solid; border-color: gray; border-width: 1px;}")
        
        # Populate
        # These are common controls so are attached to the main RX only
        if self.__rx_id == 0:
            # Alex Attenuator
            attn_label = QtGui.QLabel('Attn')
            grid.addWidget(attn_label, 0, 0, QtCore.Qt.AlignCenter)
            attn_combo = QtGui.QComboBox()
            attn_list = []
            for attn in attn_lookup:
                attn_list.append(attn[1])
            attn_combo.addItems(attn_list)
            grid.addWidget(attn_combo, 1, 0)
            attn_combo.activated.connect(self.on_attn)
            # Set the current state
            attn_combo.setCurrentIndex(self.__state[ATTN])
            
            # Main preamp
            pre_label = QtGui.QLabel('HF Preamp')
            grid.addWidget(pre_label, 0, 1, QtCore.Qt.AlignCenter)
            pre_combo = QtGui.QComboBox()
            pre_list = []
            for pre in pre_lookup:
                pre_list.append(pre[1])
            pre_combo.addItems(pre_list)
            grid.addWidget(pre_combo, 1, 1)
            pre_combo.activated.connect(self.on_preamp)
            # Set the current state
            pre_combo.setCurrentIndex(self.__state[PREAMP_HF])
            
            # Alex 6m preamp
            pre_6_label = QtGui.QLabel('6M Preamp')
            grid.addWidget(pre_6_label, 0, 2, QtCore.Qt.AlignCenter)
            pre_6_combo = QtGui.QComboBox()
            pre_6_list = []
            for pre in pre_lookup:
                pre_6_list.append(pre[1])
            pre_6_combo.addItems(pre_6_list)
            grid.addWidget(pre_6_combo, 1, 2)
            pre_6_combo.activated.connect(self.on_preamp_6m)
            # Set the current state
            pre_6_combo.setCurrentIndex(self.__state[PREAMP_6M])
        
        # Volume
        next_grid = QtGui.QGridLayout()
        gain_label = QtGui.QLabel('Volume')
        next_grid.addWidget(gain_label, 0, 0)
        gain_slider = QtGui.QSlider(QtCore.Qt.Horizontal)
        gain_slider.setTickInterval(50)
        next_grid.addWidget(gain_slider, 0, 1)
        gain_slider.setValue (self.__state[GAIN])
        gain_slider.valueChanged.connect(self.on_gain)
        grid.addLayout(next_grid, 3, 0, 1, 3)
        
        panel.setLayout(grid)
        
    # Helpers --------------------------------------------------------------
    def __set_filter(self):
        """
        Set the filter according to mode
        
        """
        
        mode = self.__state[MODE]
        filter = self.__state[FILTER]
        (_, low, high, _) = filter_lookup[filter]
        lsb = (CH_LSB, CH_CWL, CH_DIGL)
        wb = (CH_DSB, CH_FM, CH_AM, CH_SAM)
        if mode in lsb:
            low_cutoff = -high
            high_cutoff = -low
        elif mode in wb:
            low_cutoff = -high
            high_cutoff = high
        else:
            low_cutoff = low
            high_cutoff = high
            
        self.__ch_api.set_rx_filter_freq(low_cutoff, high_cutoff)
        if self.__disp != None: self.__disp.setFilterLimits(low_cutoff, high_cutoff)
    
    def __check_antenna_plan(self, last_freq, freq, force = False):
        """
        See if we need to switch antennas
        
        Arguments:
            last_freq   --  freq at the last frequency change
            freq        --  the current frequency
            force       --  force a check regardless of frequency change
        
        The antenna plan is of this form
        {
            ANT_SETTING: {ANT_1: [MIN_FREQ, MAX_FREQ],},
            ANT_PREC: 'default'
        }
        
        """
        
        if self.__rx_id == 0:
            if  (last_freq != None and \
                self.__antenna_plan != None and \
                int(last_freq) != int(freq)) or \
                force:
                # Either force a check 
                # As the frequency is a float non equality means a change of MHz digits
                # This is the cue to check the antenna selection
                f = int(freq)
                settings = self.__antenna_plan[ANT_SETTING]
                precedence = self.__antenna_plan[ANT_PREC]
                if precedence == ANT_DEFAULT:
                    # Set to ANT1
                    self.__metis_api.set_alex_tx_relay('tx1')
                    self.__metis_api.set_alex_rx_out(False)
                elif precedence == ANT_ANT:
                    # Look for the f within antenna 2/3
                    if settings[ANT_2][0] <= f and settings[ANT_2][1] >= f:
                        self.__metis_api.set_alex_tx_relay('tx2')
                        self.__metis_api.set_alex_rx_out(False)
                    elif settings[ANT_3][0] <= f and settings[ANT_3][1] >= f:
                        self.__metis_api.set_alex_tx_relay('tx3')
                        self.__metis_api.set_alex_rx_out(False)
                    else:
                        self.__metis_api.set_alex_tx_relay('tx1')
                        self.__metis_api.set_alex_rx_out(False)
                elif precedence == ANT_ANTRX:
                    # Look for the f within rx antenna 1/2/3
                    if settings[RX_1][0] <= f and settings[RX_1][1] >= f:
                        self.__metis_api.set_alex_rx_ant('rx1')
                        self.__metis_api.set_alex_rx_out(True)
                    elif settings[RX_2][0] <= f and settings[RX_2][1] >= f:
                        self.__metis_api.set_alex_rx_ant('rx2')
                        self.__metis_api.set_alex_rx_out(True)
                    elif settings[RX_3][0] <= f and settings[RX_3][1] >= f:
                        self.__metis_api.set_alex_rx_ant('xv')
                        self.__metis_api.set_alex_rx_out(True)
                    else:
                        self.__metis_api.set_alex_tx_relay('tx1')
                        self.__metis_api.set_alex_rx_out(False)
            else:
                self.__metis_api.set_alex_tx_relay('tx1')
                self.__metis_api.set_alex_rx_out(False)
            
    # Events -------------------------------------------------------------------
    def process_event(self, event_type, event_data):
        """
        Process system events
        
        Arguments:
            event_type    --  managed events from defs.py
            event_data    --  call specific data
            
        """
        
        if event_type == TX_FREQ_EVNT:
            self.__current_freq = event_data
            self.__vfo.set_freq(self.__current_freq)
            self.__check_antenna_plan(self.__last_freq, self.__current_freq, True)
        elif event_type == ANTENNA_EVNT:
            self.__cfg = instance_cache.getInstance('Settings')
            self.__antenna_plan = self.__cfg[COMMON][ANTENNA].copy.deepcopy()
            self.__check_antenna_plan(self.__last_freq, self.__current_freq, True)
        elif event_type == DISPLAY_EVNT:
            self.__cfg = instance_cache.getInstance('Settings')
            if  self.__current_cfg[RX][DISPLAYS][PAN_MODE] != self.__cfg[RX][DISPLAYS][PAN_MODE] or\
                self.__current_cfg[RX][DISPLAYS][PAN_FRM_AV] != self.__cfg[RX][DISPLAYS][PAN_FRM_AV] or\
                self.__current_cfg[RX][DISPLAYS][WINDOW_TYPE] != self.__cfg[RX][DISPLAYS][WINDOW_TYPE] or\
                self.__current_cfg[RX][DISPLAYS][PAN_FFT_SZ] != self.__cfg[RX][DISPLAYS][PAN_FFT_SZ]:
                # We have a change in display parameters
                self.__disp.set_display(self.__blk_sz, self.__cfg[RX][DISPLAYS][PAN_MODE], self.__cfg[RX][DISPLAYS][PAN_FRM_AV], self.__cfg[RX][DISPLAYS][WINDOW_TYPE], self.__cfg[RX][DISPLAYS][PAN_FFT_SZ])                       
            
    # Callbacks ----------------------------------------------------------------
    def __panFreq(self, freq):
        """
        Callback from click-tune
        
        Arguments:
            freq    --  click-tune frequency
        """
        
        self.__current_freq = freq
        self.__vfo.set_freq(self.__current_freq)
        if self.__disp != None:
            self.__disp.setCenterFreq(self.__current_freq)

    def __floatCallback(self, floated, width):
        """
        Callback from display when window is floated/docked
        
        Arguments:
            floated     --  True if floated
            width       --  Current display width
            
        """
        
        if floated:
            # Set the size to nominal
            self.__resize_to = (self.__min_width, self.__height)
        else:
            # Set to the extended size
            self.__resize_to = (self.__min_width + width, self.__height)
        # Apply in 100ms. Applying in the callback does not work
        QtCore.QTimer.singleShot(100, self.__timeout)
    
    def __timeout(self):
        """ Callback from single-shot timeout """
        
        self.resize(self.__resize_to[0], self.__resize_to[1])
        